/* NOTICE
    OpenJaWS - Open Java Weather Station
    
    Copyright (C) 2008 Grant Gardner <grant@lastweekend.com.au>
    
    OpenJaWS is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
    
NOTICE */
package au.com.lastweekend.ws2300.proxy;

import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import au.com.lastweekend.openjaws.util.LineGobbler;
import au.com.lastweekend.openjaws.util.StringUtil;
import au.com.lastweekend.ws2300.Device;
import au.com.lastweekend.ws2300.RawDevice;
import au.com.lastweekend.ws2300.SerialDevice;

/**
 * Implements the device side of the WS2300 serial protocol.
 * <p>
 * Can be backed by any DeviceInterface, including a real WS2300 via
 * SerialDevice.
 * <p>
 * Useful with a serial interceptor such as "interceptty"
 * 
 * @author ggardner
 * 
 */
public class WS2300Proxy extends Thread {

	public class LoggingOutputStream extends BufferedOutputStream {

		public LoggingOutputStream(OutputStream out) {

			super(out);

		}

		public String getBuffer() {

			return StringUtil.toHexBytes(buf, 0, count);
		}

	}

	public enum State {
		ADDRESS, ERROR, ADDRESS_COMPLETE, RECEIVE_DATA
	}

	private static final Logger LOG = LoggerFactory
			.getLogger(WS2300Proxy.class);

	private InputStream inputStream;
	private LoggingOutputStream outputStream;
	private boolean stopped = false;
	private State state = State.ADDRESS;
	private int address = 0;
	private int addressCount = 0;
	private Device backDevice;

	public WS2300Proxy(InputStream in, OutputStream out, Device backDevice) {

		inputStream = in;
		outputStream = new LoggingOutputStream(out);
		if (backDevice == null) {
			backDevice = new MemoryDevice();
		}

		this.backDevice = backDevice;
	}

	@Override
	public void run() {

		while (!stopped) {
			try {

				int command = inputStream.read();
				if (command < 0) {
					LOG.debug("Input stream closed. Stopping");
					stopped = true;
					continue;
				}
				int result = process(command);
				outputStream.write(result);
				LOG.debug(String.format("Read: %02x", command) + ", Writing:"
						+ outputStream.getBuffer());
				outputStream.flush();

			} catch (IOException e) {
				state = State.ERROR;
				LOG.error("Stopping due to exception", e);
				stopped = true;
			}
		}
	}

	private int process(int command) throws IOException {

		// cc .. .. 10, cc = command switch
		switch (command & RawDevice.COMMAND_READ) {

		case (RawDevice.COMMAND_READ):
			// 11 nn nn 10, n = numBytesToRead
			return read((command >> 2) & 0xF);
		case (RawDevice.COMMAND_ADDRESS):
			// 10 aa aa 10, a = addressNibble
			return address((command >> 2) & 0xF);

		case (RawDevice.COMMAND_WRITE):
			// 01 vv vv 10, v = nibbleValue
			return writeNibble((command >> 2) & 0xF);

		case (RawDevice.COMMAND_ALL):
			// 00 cc .. 10, cc = command switch
			switch (command & RawDevice.COMMAND_UNSET_BIT) {
			case (RawDevice.COMMAND_UNSET_BIT):
				// 00 11 bb 10, b = bitToUnset
				return setBit((command >> 2) & 0x3, false);

			case (RawDevice.COMMAND_SET_BIT):
				// 00 01 bb 10, b = bitToSet
				return setBit((command >> 2) & 0x3, true);

			case (RawDevice.COMMAND_ALL):
				// 00 00 cc 10
				switch (command & 0x0E) {
				case (RawDevice.COMMAND_RESET):
					// 00 00 01 10, reset.
					return reset();
				default:
				}
			default:
			}
		default:
			LOG.error("Invalid command " + Integer.toBinaryString(command));
			return error();

		}
	}

	private int read(int numBytes) throws IOException {

		if (state != State.ADDRESS_COMPLETE) {
			return error();
		}

		outputStream.write(0x30 + numBytes);

		int checksum = 0;

		byte[] readNibbles = backDevice.read(address, numBytes);

		byte[] compressedNibbles = new byte[numBytes];

		for (int i = 0; i < numBytes; i++) {
			compressedNibbles[i] = (byte) ((readNibbles[2 * i + 1] << 4) + readNibbles[2 * i]);
			checksum += compressedNibbles[i];
		}
		outputStream.write(compressedNibbles);

		checksum = checksum & 0xFF;

		state = State.ADDRESS;
		return checksum;
	}

	private int setBit(int bitAddress, boolean setBit) throws IOException {

		if (state != State.ADDRESS_COMPLETE) {
			return error();
		}

		backDevice.writeBit(address, bitAddress, setBit);

		int bitCheck = (setBit ? RawDevice.ACK_SET_BIT
				: RawDevice.ACK_UNSET_BIT)
				+ bitAddress;

		// ready for the next address
		state = State.ADDRESS;
		return bitCheck;

	}

	private int writeNibble(int value) throws IOException {

		if (state == State.ADDRESS_COMPLETE) {
			state = State.RECEIVE_DATA;
		}

		if (state != State.RECEIVE_DATA) {
			LOG.warn("Bad state " + state + " while trying to write nibble");
			return error();
		}

		backDevice.write(address, new byte[] { (byte) value });

		address++;

		return RawDevice.ACK_WRITE + value;
	}

	private int address(int addressNibble) {

		if (state == State.RECEIVE_DATA) {
			state = State.ADDRESS;
		}

		if (state != State.ADDRESS || addressCount > 3) {
			return error();
		}

		if (addressCount == 0) {
			address = 0;
		}

		address = (address << 4) + addressNibble;
		int checksum = (addressCount << 4) + addressNibble;
		addressCount += 1;

		if (addressCount == 4) {
			state = State.ADDRESS_COMPLETE;
			addressCount = 0;
		}

		return checksum;

	}

	private int reset() {

		state = State.ADDRESS;
		addressCount = 0;
		return 0x02;

	}

	private int error() {

		state = State.ERROR;
		return 0xFF;
	}

	public static Process interceptty(final Logger baseLogger,
			String serialPort, String... proxyArgs) throws IOException {

		final Logger intercepttyLogger = LoggerFactory.getLogger(baseLogger
				.getName()
				+ ".interceptty");

		String maven = System.getProperty("localRepository");
		String classPath;
		if (maven == null) {
			classPath = System.getProperty("java.class.path");
		} else {
			// Horrendous hack because we can't get the dependencies
			// from maven directly
			String baseDir = System.getProperty("basedir");
			classPath = baseDir + "/target/classes:" + baseDir
					+ "/target/test-classes:" + baseDir
					+ "/../../modules/common/target/classes:" + maven
					+ "/org/slf4j/slf4j-api/1.4.3/slf4j-api-1.4.3.jar:" + maven
					+ "/org/slf4j/slf4j-simple/1.4.3/slf4j-simple-1.4.3.jar";

		}

		ProcessBuilder builder = new ProcessBuilder();
		List<String> args = new ArrayList<String>();
		args.add("interceptty");
		if (!intercepttyLogger.isDebugEnabled()) {
			args.add("-q");
		}

		StringBuilder proxyCommand = new StringBuilder("!java -cp ");
		proxyCommand.append(classPath);
		proxyCommand.append(' ');

		boolean jvmArgs = true;
		for (String proxyArg : proxyArgs) {
			if (jvmArgs && !proxyArg.startsWith("-")) {
				proxyCommand.append(' ');
				proxyCommand.append(WS2300Proxy.class.getCanonicalName());
				jvmArgs = false;
			}
			proxyCommand.append(' ');
			proxyCommand.append(proxyArg);
		}

		if (jvmArgs) {
			throw new IOException(
					"No args supplied to WS2300Proxy behind interceptty");
		}

		args.add(proxyCommand.toString());

		args.add(serialPort);
		builder.command(args);

		Process intercepttyProcess = builder.start();
		LineGobbler errorGobbler = new LineGobbler(intercepttyProcess
				.getErrorStream()) {

			@Override
			protected void gobble(String line) {
				// Logging of the interceptty has to go to stderr because stdin
				// is the tty
				intercepttyLogger.info(line);
			}

			@Override
			protected void handleReaderException(IOException e) {

				baseLogger.debug("Exception gobbling interceptty error output",
						e);
			}
		};
		new Thread(errorGobbler).start();

		if (intercepttyLogger.isDebugEnabled()) {
			LineGobbler outputGobbler = new LineGobbler(intercepttyProcess
					.getInputStream()) {

				@Override
				protected void gobble(String line) {

					intercepttyLogger.debug(line);
				}

				@Override
				protected void handleReaderException(IOException e) {

					baseLogger
							.debug("Exception gobbling interceptty output", e);
				}

			};
			new Thread(outputGobbler).start();
		}

		baseLogger.debug("Started: " + builder.command().toString());

		return intercepttyProcess;
	}

	/**
	 * <pre>
	 *  Usage: WS2300Proxy i|m|s args
	 *  i backDevice [-javaArgs] [proxyArgs]- start WS2300Proxy behind interceptty
	 *  m [dumpFile] - start in-memory proxy, optionally loaded from dumpFile
	 *  s serialPort - start serial device proxy
	 * </pre>
	 * 
	 * @throws IOException
	 */
	public static void main(String[] args) throws IOException {

		if (args.length == 0) {
			usage();
			return;
		}

		if ("i".equals(args[0])) {
			// i backdevice proxyArgs
			String[] newArgs = new String[args.length - 2];
			System.arraycopy(args, 2, newArgs, 0, newArgs.length);
			final Process interceptty = interceptty(LOG, args[1], newArgs);
			Runtime.getRuntime().addShutdownHook(new Thread() {

				public void run() {

					interceptty.destroy();
				}
			});
			LOG.info("Started interceptty" + StringUtil.toString(args));
			return;
		}

		Device device;
		if ("m".equals(args[0])) {
			// m [loadFile]
			device = new MemoryDevice();
			if (args.length > 1) {
				device.load(new InputStreamReader(WS2300Proxy.class
						.getResourceAsStream(args[1])));
			}
		} else if ("s".equals(args[0])) {
			// s serialPort
			device = new SerialDevice(args[0]);
		} else {
			usage();
			return;
		}

		WS2300Proxy ws2300 = new WS2300Proxy(System.in, System.out, device);
		ws2300.start();
		LOG.info("Started Proxy " + StringUtil.toString(args));
	}

	private static void usage() {

		System.err.println("Usage: WS2300Proxy i|m|s args");
		System.err
				.println("i backDevice [-javaArgs] [proxyArgs]- start WS2300Proxy behind interceptty");
		System.err
				.println("m [dumpFile] - start in-memory proxy, optionally loaded from dumpFile");
		System.err.println("s serialPort - start serial device proxy");
		System.exit(1);
	}
}
